/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.core;

import org.apache.catalina.*;
import org.apache.catalina.util.LifecycleBase;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;

import javax.management.ObjectName;
import java.util.ArrayList;

/**
 * Standard implementation of a processing <b>Pipeline</b> that will invoke
 * a series of Valves that have been configured to be called in order.  This
 * implementation can be used for any type of Container.
 * <p>
 * <b>IMPLEMENTATION WARNING</b> - This implementation assumes that no
 * calls to <code>addValve()</code> or <code>removeValve</code> are allowed
 * while a request is currently being processed.  Otherwise, the mechanism
 * by which per-thread state is maintained will need to be modified.
 *
 * @author Craig R. McClanahan
 */

public class StandardPipeline extends LifecycleBase
		implements Pipeline, Contained {

	/**
	 * Descriptive information about this implementation.
	 */
	protected static final String info = "org.apache.catalina.core.StandardPipeline/1.0";

	// ----------------------------------------------------------- Constructors
	private static final Log log = LogFactory.getLog(StandardPipeline.class);
	/**
	 * The basic Valve (if any) associated with this Pipeline.
	 */
	protected Valve basic = null;


	// ----------------------------------------------------- Instance Variables
	/**
	 * The Container with which this Pipeline is associated.
	 */
	protected Container container = null;
	/**
	 * The first valve associated with this Pipeline.
	 */
	protected Valve first = null;

	/**
	 * Construct a new StandardPipeline instance with no associated Container.
	 */
	public StandardPipeline() {

		this(null);

	}

	/**
	 * Construct a new StandardPipeline instance that is associated with the
	 * specified Container.
	 *
	 * @param container The container we should be associated with
	 */
	public StandardPipeline(Container container) {

		super();
		setContainer(container);

	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Return descriptive information about this implementation class.
	 */
	public String getInfo() {

		return info;

	}

	@Override
	public boolean isAsyncSupported() {
		Valve valve = (first != null) ? first : basic;
		boolean supported = true;
		while (supported && valve != null) {
			supported = supported & valve.isAsyncSupported();
			valve = valve.getNext();
		}
		return supported;
	}


	// ------------------------------------------------------ Contained Methods

	/**
	 * Return the Container with which this Pipeline is associated.
	 */
	@Override
	public Container getContainer() {

		return (this.container);

	}

	/**
	 * Set the Container with which this Pipeline is associated.
	 *
	 * @param container The new associated container
	 */
	@Override
	public void setContainer(Container container) {

		this.container = container;

	}

	@Override
	protected void initInternal() {
		// NOOP
	}

	/**
	 * Start {@link Valve}s) in this pipeline and implement the requirements
	 * of {@link LifecycleBase#startInternal()}.
	 *
	 * @throws LifecycleException if this component detects a fatal error
	 *                            that prevents this component from being used
	 */
	@Override
	protected synchronized void startInternal() throws LifecycleException {

		// Start the Valves in our pipeline (including the basic), if any
		Valve current = first;
		if (current == null) {
			current = basic;
		}
		while (current != null) {
			if (current instanceof Lifecycle)
				((Lifecycle) current).start();
			current = current.getNext();
		}

		setState(LifecycleState.STARTING);
	}

	/**
	 * Stop {@link Valve}s) in this pipeline and implement the requirements
	 * of {@link LifecycleBase#stopInternal()}.
	 *
	 * @throws LifecycleException if this component detects a fatal error
	 *                            that prevents this component from being used
	 */
	@Override
	protected synchronized void stopInternal() throws LifecycleException {

		setState(LifecycleState.STOPPING);

		// Stop the Valves in our pipeline (including the basic), if any
		Valve current = first;
		if (current == null) {
			current = basic;
		}
		while (current != null) {
			if (current instanceof Lifecycle)
				((Lifecycle) current).stop();
			current = current.getNext();
		}
	}

	@Override
	protected void destroyInternal() {
		Valve[] valves = getValves();
		for (Valve valve : valves) {
			removeValve(valve);
		}
	}

	/**
	 * Return a String representation of this component.
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("Pipeline[");
		sb.append(container);
		sb.append(']');
		return sb.toString();
	}


	// ------------------------------------------------------- Pipeline Methods

	/**
	 * <p>Return the Valve instance that has been distinguished as the basic
	 * Valve for this Pipeline (if any).
	 */
	@Override
	public Valve getBasic() {

		return (this.basic);

	}

	/**
	 * <p>Set the Valve instance that has been distinguished as the basic
	 * Valve for this Pipeline (if any).  Prior to setting the basic Valve,
	 * the Valve's <code>setContainer()</code> will be called, if it
	 * implements <code>Contained</code>, with the owning Container as an
	 * argument.  The method may throw an <code>IllegalArgumentException</code>
	 * if this Valve chooses not to be associated with this Container, or
	 * <code>IllegalStateException</code> if it is already associated with
	 * a different Container.</p>
	 *
	 * @param valve Valve to be distinguished as the basic Valve
	 */
	@Override
	public void setBasic(Valve valve) {

		// Change components if necessary
		Valve oldBasic = this.basic;
		if (oldBasic == valve)
			return;

		// Stop the old component if necessary
		if (oldBasic != null) {
			if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) {
				try {
					((Lifecycle) oldBasic).stop();
				} catch (LifecycleException e) {
					log.error("StandardPipeline.setBasic: stop", e);
				}
			}
			if (oldBasic instanceof Contained) {
				try {
					((Contained) oldBasic).setContainer(null);
				} catch (Throwable t) {
					ExceptionUtils.handleThrowable(t);
				}
			}
		}

		// Start the new component if necessary
		if (valve == null)
			return;
		if (valve instanceof Contained) {
			((Contained) valve).setContainer(this.container);
		}
		if (getState().isAvailable() && valve instanceof Lifecycle) {
			try {
				((Lifecycle) valve).start();
			} catch (LifecycleException e) {
				log.error("StandardPipeline.setBasic: start", e);
				return;
			}
		}

		// Update the pipeline
		Valve current = first;
		while (current != null) {
			if (current.getNext() == oldBasic) {
				current.setNext(valve);
				break;
			}
			current = current.getNext();
		}

		this.basic = valve;

	}

	/**
	 * <p>Add a new Valve to the end of the pipeline associated with this
	 * Container.  Prior to adding the Valve, the Valve's
	 * <code>setContainer()</code> method will be called, if it implements
	 * <code>Contained</code>, with the owning Container as an argument.
	 * The method may throw an
	 * <code>IllegalArgumentException</code> if this Valve chooses not to
	 * be associated with this Container, or <code>IllegalStateException</code>
	 * if it is already associated with a different Container.</p>
	 *
	 * @param valve Valve to be added
	 * @throws IllegalArgumentException if this Container refused to
	 *                                  accept the specified Valve
	 * @throws IllegalArgumentException if the specified Valve refuses to be
	 *                                  associated with this Container
	 * @throws IllegalStateException    if the specified Valve is already
	 *                                  associated with a different Container
	 */
	@Override
	public void addValve(Valve valve) {

		// Validate that we can add this Valve
		if (valve instanceof Contained)
			((Contained) valve).setContainer(this.container);

		// Start the new component if necessary
		if (getState().isAvailable()) {
			if (valve instanceof Lifecycle) {
				try {
					((Lifecycle) valve).start();
				} catch (LifecycleException e) {
					log.error("StandardPipeline.addValve: start: ", e);
				}
			}
		}

		// Add this Valve to the set associated with this Pipeline
		if (first == null) {
			first = valve;
			valve.setNext(basic);
		} else {
			Valve current = first;
			while (current != null) {
				if (current.getNext() == basic) {
					current.setNext(valve);
					valve.setNext(basic);
					break;
				}
				current = current.getNext();
			}
		}

		container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
	}

	/**
	 * Return the set of Valves in the pipeline associated with this
	 * Container, including the basic Valve (if any).  If there are no
	 * such Valves, a zero-length array is returned.
	 */
	@Override
	public Valve[] getValves() {

		ArrayList<Valve> valveList = new ArrayList<Valve>();
		Valve current = first;
		if (current == null) {
			current = basic;
		}
		while (current != null) {
			valveList.add(current);
			current = current.getNext();
		}

		return valveList.toArray(new Valve[0]);

	}

	public ObjectName[] getValveObjectNames() {

		ArrayList<ObjectName> valveList = new ArrayList<ObjectName>();
		Valve current = first;
		if (current == null) {
			current = basic;
		}
		while (current != null) {
			if (current instanceof ValveBase) {
				valveList.add(((ValveBase) current).getObjectName());
			}
			current = current.getNext();
		}

		return valveList.toArray(new ObjectName[0]);

	}

	/**
	 * Remove the specified Valve from the pipeline associated with this
	 * Container, if it is found; otherwise, do nothing.  If the Valve is
	 * found and removed, the Valve's <code>setContainer(null)</code> method
	 * will be called if it implements <code>Contained</code>.
	 *
	 * @param valve Valve to be removed
	 */
	@Override
	public void removeValve(Valve valve) {

		Valve current;
		if (first == valve) {
			first = first.getNext();
			current = null;
		} else {
			current = first;
		}
		while (current != null) {
			if (current.getNext() == valve) {
				current.setNext(valve.getNext());
				break;
			}
			current = current.getNext();
		}

		if (first == basic) first = null;

		if (valve instanceof Contained)
			((Contained) valve).setContainer(null);

		if (valve instanceof Lifecycle) {
			// Stop this valve if necessary
			if (getState().isAvailable()) {
				try {
					((Lifecycle) valve).stop();
				} catch (LifecycleException e) {
					log.error("StandardPipeline.removeValve: stop: ", e);
				}
			}
			try {
				((Lifecycle) valve).destroy();
			} catch (LifecycleException e) {
				log.error("StandardPipeline.removeValve: destroy: ", e);
			}
		}

		container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve);
	}

	@Override
	public Valve getFirst() {
		if (first != null) {
			return first;
		}

		return basic;
	}
}
